package com.xuzimian.globaldemo.spring.springmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.ExecutorService;

/**
 * SSE(Server-Sent Events) : 本质上，这种通信就是以流信息的方式，完成一次用时很长的下载.
 *
 * 严格地说，HTTP 协议无法做到服务器主动推送信息。但是，有一种变通方法，就是服务器向客户端声明，
 * 接下来要发送的是流信息（streaming）。也就是说，发送的不是一次性的数据包，而是一个数据流，
 * 会连续不断地发送过来。这时，客户端不会关闭连接，会一直等着服务器发过来的新的数据流，视频播放
 * 就是这样的例子。本质上，这种通信就是以流信息的方式，完成一次用时很长的下载。
 * SSE 就是利用这种机制，使用流信息向浏览器推送信息。它基于 HTTP 协议，目前除了 IE/Edge，其他浏览器都支持。
 *
 * Server-sent Events 规范是 HTML 5 规范的一个组成部分，具体的规范文档见参考资源。该规范比较简单，主要由两个部分组成：
 * 第一个部分是服务器端与浏览器端之间的通讯协议，第二部分则是在浏览器端可供 JavaScript 使用的 EventSource 对象。
 * 通讯协议是基于纯文本的简单协议。 服务器端的响应的内容类型是“text/event-stream”。响应文本的内容可以看成是一个事件流，由不同的事件所组成。
 * 每个事件由类型和数据两部分组成，同时每个事件可以有一个可选的标识符。
 * 不同事件的内容之间通过仅包含回车符和换行符的空行（“\r\n”）来分隔。每个事件的数据可能由多行组成。
 *
 * SSE 的优点:
 * SSE 使用 HTTP 协议，现有的服务器软件都支持。WebSocket 是一个独立协议。
 * SSE 属于轻量级，使用简单；WebSocket 协议相对复杂。
 * SSE 默认支持断线重连，WebSocket 需要自己实现。
 * SSE 一般只用来传送文本，二进制数据需要编码后传送，WebSocket 默认支持传送二进制数据。
 * SSE 支持自定义发送的消息类型。
 *
 * -------------------------------------------------------------------------------------------------------------
 *
 * 客户端：
 *
 * SSE 的客户端 API 部署在EventSource对象上。下面的代码可以检测浏览器是否支持 SSE。
 * if ('EventSource' in window) {}
 * 使用 SSE 时，浏览器首先生成一个EventSource实例，向服务器发起连接。
 * var source = new EventSource(url);
 *
 * 服务端：
 * 服务器向浏览器发送的 SSE 数据，必须是 UTF-8 编码的文本，具有如下的 HTTP 头信息：
 * Content-Type: text/event-stream
 * Cache-Control: no-cache
 * Connection: keep-alive
 * 上面三行之中，第一行的Content-Type必须指定 MIME 类型为event-steam。
 * 每一次发送的信息，由若干个message组成，每个message之间用\n\n分隔。每个message内部由若干行组成，每一行都是如下格式。
 * [field]: value\n
 *
 * 上面的field可以取四个值：data ,event, id, retry
 * 此外，还可以有冒号开头的行，表示注释。通常，服务器每隔一段时间就会向浏览器发送一个注释，保持连接不中断
 * : This is a comment
 *
 * 1.data字段
 *   数据内容用data字段表示: data:  message\n\n
 *   如果数据很长，可以分成多行，最后一行用\n\n结尾，前面行都用\n结尾。
 *   data: begin message\n
 *   data: continue message\n\n
 *
 * 2.id 字段
 *   数据标识符用id字段表示，相当于每一条数据的编号。
     id: msg1\n
 *   data: message\n\n
 *
 * 3.event 字段
 *   event字段表示自定义的事件类型，默认是message事件。浏览器可以用addEventListener()监听该事件。
 *
 * 4.retry 字段
 *   服务器可以用retry字段，指定浏览器重新发起连接的时间间隔。
 *   retry: 10000\n
 *   两种情况会导致浏览器重新发起连接：一种是时间间隔到期，二是由于网络错误等原因，导致连接出错。
 *
 * -------------------------------------------------------------------------------------------------------
 *
 * 1.Ajax短轮询：脚本发送的http请求。传统的web应用要想与服务器交互，必须提交一个表单（form），服务器接收并处理传来的表单，
 *   然后返回全新的页面，因为前后两个页面的数据大部分都是相同的，这个过程传输了很多冗余的数据、浪费了带宽。于是Ajax技术便应运而生。
 *
 *   短轮询与长轮询的区别是，服务器接收到请求后，是否立即发送响应。短轮询是服务器会立即发送响应，无论数据是否有效。而长轮询是等待发送响应。
 *   所有浏览器都支持轮询，使用xhr对象和setTimeOut()就能实现。
 *
 * 2.长轮询(Comet)的方式在每次请求时，服务器端会保持该连接在一段时间内处于打开状态，而不是在响应完成之后就立即关闭。
 *   这样做的好处是在连接处于打开状态的时间段内，服务器端产生的数据更新可以被及时地返回给浏览器。
 *   当上一个长连接关闭之后，浏览器会立即打开一个新的长连接来继续请求。
 *
 *   Comet的实现主要有两种方式，基于Ajax的长轮询（long-polling）方式和基于 Iframe 及 htmlfile 的流（http streaming）方式。
 *   2.1 基于Ajax的长轮询（long-polling）方式：
 *       浏览器发出XMLHttpRequest 请求，服务器端接收到请求后，会阻塞请求直到有数据或者超时才返回，浏览器JS在处理请求返回信息（超时或有效数据）
 *       后再次发出请求，重新建立连接。在此期间服务器端可能已经有新的数据到达，服务器会选择把数据保存，直到重新建立连接，浏览器会把所有数据一次性取回。
 *
 *   2.2 长连接——基于 Iframe 及 htmlfile 的流（http streaming）方式：
 *       在页面里嵌入一个隐蔵iframe，将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求，服务器端就能源源不断地往客户端输入数据。
 *       Iframe是html标记，这个标记的src属性会保持对指定服务器的长连接请求，服务器端则可以不停地返回数据，相对于第一种方式，这种方式跟传统的服务器推则更接近。
 *       在第一种方式中，浏览器在收到数据后会直接调用JS回调函数，但是这种方式该如何响应数据呢？可以通过在返回数据中嵌入JS脚本的方式，
 *       如“<script type="text/javascript">js_func(“data from server ”)</script>”，服务器端将返回的数据作为回调函数的参数，
 *       浏览器在收到数据后就会执行这段JS脚本。
 *
 *       推送也叫做长连接。也就是客户端并不是隔一段时间请求一次，而是发起一次请求后就挂起，一直到服务端有更新的时候，服务器才会主动推送信息到客户端。
 *       如果没有更新信息，那就一直保持连接的状态，客户端不做多余的请求，服务器端也不做响应。
 *
 *       长轮询是对定时轮询的一种改进，目的是为了降低无效的网络传输。当服务器端没有数据更新的时候，连接会保持一段时间周期直到数据改变或时间过期。
 *       通过这种机制来减少无效的客户端和服务器之间的交互。
 *
 * 3.HTTP流（HTTP Streaming），浏览器向服务器发送一个请求，而服务器保持连接打开，然后周期性地向浏览器发送数据。
 *   http-stream流的通信方式，其原理是让客户端在一次请求中保持和服务端连接不断开，然后服务端源源不断传送数据给客户端，就好比数据流一样，并不是一次性将数据全部发给客户端。
 *   它与polling方式的区别在于整个通信过程客户端只发送一次请求，然后服务端保持与客户端的长连接，并利用这个连接在回送数据给客户端。
 *
 * 4.SSE(Server-Sent Events,服务器发送事件)用于创建到服务器的单向连接，服务器通过这个连接可以发送任意数量的数据。
 *   服务器响应的MIME类型必须是text/even-stream，而且是浏览器中的JavaScript API能解析格式输出。SSE支持短轮询、
 *   长轮询和HTTP流，而且能在断开连接时自动确定何时重新连接。
 *
 * 总结：结合比较上面提到的 4 种不同的技术，简易轮询由于其本身的缺陷，并不推荐使用。Comet 技术并不是 HTML 5 标准的一部分，从兼容标准的角度出发，
 * 也不推荐使用。WebSocket 规范和服务器推送技术都是 HTML 5 标准的组成部分，在主流浏览器上都提供了原生的支持，是推荐使用的。不过 WebSocket
 * 规范更加复杂一些，适用于需要进行复杂双向数据通讯的场景。对于简单的服务器数据推送的场景，使用服务器推送（SSE技术）事件就足够了。
 *
 *
 * 长连接的建立:
 * 1.建立HTTP长连接，HTTP请求头包含如下内容：Connection：Keep-Alive
 * 2.服务端同意建立长连接，HTTP响应头包含如下内容：Connection：Keep-Alive
 * 3.关闭连接时，HTTP头包含：Connection：Close
 *
 * 参考内容链接：http://www.52im.net/thread-336-1-1.html
 *
 * @program: global-demo
 * @description: Server-Sent Events Demo
 * @author: xzm
 * @create: 2019-04-25 10:11
 **/
@RequestMapping("/sse")
@Controller
public class SSEController {

    /**
     * 可以使用DeferredResult和Callable作为单个异步返回值。如果要生成多个异步值并将其写入响应，
     * 可以使用ResponseBodyEmitter返回值来生成对象流。其中每个对象都用httpMessageConverter序列化并写入响应。
     * 用于异步的写入多个消息。
     * @return
     */
    @RequestMapping("/hello")
    public ResponseBodyEmitter hello() {
        SseEmitter emitter = new SseEmitter();
        Runnable task = () -> {
            try {
                Thread.sleep(1000  * 10);
                emitter.send("Hello");
                emitter.send("Bye");
                emitter.complete();
            } catch (IOException e) {
                e.printStackTrace();
                emitter.completeWithError(e);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        try {
            emitter.send("你好！");
        } catch (IOException e) {
            e.printStackTrace();
        }
        new Thread(task).start();
        return emitter;
    }

    /**
     * 作用和ResponseBodyEmitter类似，也是异步的写入多个消息,区别在于它使用的是Server-Sent Events.
     * @return
     */
    @RequestMapping("/hello2")
    public SseEmitter hello2() {
        SseEmitter emitter = new SseEmitter();
        Runnable task = () -> {
            try {
                Thread.sleep(1000  * 30);
                emitter.send("Hello2");
                emitter.send("Bye2");
                emitter.complete();
            } catch (IOException e) {
                e.printStackTrace();
                emitter.completeWithError(e);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        try {
            emitter.send("你好2！");
        } catch (IOException e) {
            e.printStackTrace();
        }
        new Thread(task).start();
        return emitter;
    }
}
